import java.nio.ByteBuffer;

public class EntryMap {
	private static final int INT_SIZE = 4;
	public static final int ENTRY_SIZE_BYTES = 12;
	private static final double HASH_MAP_ENTRY_RATIO = 0.1;

	private static final int NULL = -1;

	private ByteBuffer buf;

	private int hashMapCount;
	private int entryMapCount;
	private int entryMapOffset;

	private int freeEntryHead;

	class Entry {
		public int index;
		public int key;
		public int value;
		public int next;

		public void write() {
			int entryOffset = entryMapOffset + index * ENTRY_SIZE_BYTES;

			buf.putInt(entryOffset, key);
			buf.putInt(entryOffset + INT_SIZE, value);
			buf.putInt(entryOffset + 2 * INT_SIZE, next);
		}

		public Entry read(int index) {
			if (index == NULL) {
				this.index = NULL;
			} else {
				this.index = index;

				int entryOffset = entryMapOffset + index * ENTRY_SIZE_BYTES;

				key = buf.getInt(entryOffset);
				value = buf.getInt(entryOffset + INT_SIZE);
				next = buf.getInt(entryOffset + 2 * INT_SIZE);
			}

			return this;
		}

		public boolean isNull() {
			return index == NULL;
		}

		public Entry next() {
			Entry e = new Entry();
			e.read(next);
			return e;
		}
	}

	public EntryMap(ByteBuffer buf) {
		this.buf = buf;
		init();
	}

	private void init() {
		int bufSize = buf.capacity();

		hashMapCount = (int) (bufSize / ENTRY_SIZE_BYTES * HASH_MAP_ENTRY_RATIO);

		int hashMapSize = hashMapCount * INT_SIZE;

		entryMapOffset = hashMapSize;

		entryMapCount = (bufSize - hashMapSize) / ENTRY_SIZE_BYTES;

		initHashMap();
		initEntryMap();
	}

	public int getHashMapCount() {
		return hashMapCount;
	}

	private void initEntryMap() {
		Entry e = new Entry();

		for (int index = 0; index < entryMapCount; index++) {
			e.index = index;
			if (index < entryMapCount - 1) {
				e.next = index + 1;
			} else {
				e.next = NULL;
			}
			e.write();
		}

		freeEntryHead = 0;
	}

	private void initHashMap() {
		for (int index = 0; index < hashMapCount; index++) {
			writeHashMapLink(index, NULL);
		}
	}

	private void writeHashMapLink(int index, int link) {
		buf.putInt(index * INT_SIZE, link);
	}

	private int readHashMapLink(int index) {
		return buf.getInt(index * INT_SIZE);
	}

	public Entry allocateEntry() throws OutOfBufferSpaceException {
		if (freeEntryHead != NULL) {
			Entry e = new Entry().read(freeEntryHead);
			freeEntryHead = e.next;
			e.next = NULL;
			return e;
		} else {
			throw new OutOfBufferSpaceException();
		}
	}

	public void disposeEntry(Entry e) {
		e.next = freeEntryHead;
		freeEntryHead = e.index;
		e.write();
	}

	private Entry readEntry(int entryIndex) {
		Entry e = new Entry();
		e.read(entryIndex);
		return e;
	}

	public void reset() {
		init();
	}

	public int getEntryMapCount() {
		return entryMapCount;
	}

	public Entry readHeadEntry(int hash) {
		int entryIndex = readHashMapLink(hash);
		return readEntry(entryIndex);
	}

	public void writeHeadEntry(int hash, Entry e) {
		writeHashMapLink(hash, e.index);
	}
}
